home *** CD-ROM | disk | FTP | other *** search
/ Aminet 32 / Aminet 32 (1999)(Schatztruhe)[!][Aug 1999].iso / Aminet / dev / lang / Python151.lha / Python-1.5 / Lib / Python1.5 / urllib.py < prev    next >
Encoding:
Python Source  |  1998-04-11  |  26.2 KB  |  985 lines

  1. # Open an arbitrary URL
  2. #
  3. # See the following document for more info on URLs:
  4. # "Names and Addresses, URIs, URLs, URNs, URCs", at
  5. # http://www.w3.org/pub/WWW/Addressing/Overview.html
  6. #
  7. # See also the HTTP spec (from which the error codes are derived):
  8. # "HTTP - Hypertext Transfer Protocol", at
  9. # http://www.w3.org/pub/WWW/Protocols/
  10. #
  11. # Related standards and specs:
  12. # - RFC1808: the "relative URL" spec. (authoritative status)
  13. # - RFC1738 - the "URL standard". (authoritative status)
  14. # - RFC1630 - the "URI spec". (informational status)
  15. #
  16. # The object returned by URLopener().open(file) will differ per
  17. # protocol.  All you know is that is has methods read(), readline(),
  18. # readlines(), fileno(), close() and info().  The read*(), fileno()
  19. # and close() methods work like those of open files. 
  20. # The info() method returns a mimetools.Message object which can be
  21. # used to query various info about the object, if available.
  22. # (mimetools.Message objects are queried with the getheader() method.)
  23.  
  24. import string
  25. import socket
  26. import os
  27. import sys
  28.  
  29.  
  30. __version__ = '1.10'
  31.  
  32. MAXFTPCACHE = 10        # Trim the ftp cache beyond this size
  33.  
  34. # Helper for non-unix systems
  35. if os.name == 'mac':
  36.     from macurl2path import url2pathname, pathname2url
  37. elif os.name == 'nt':
  38.     from nturl2path import url2pathname, pathname2url 
  39. else:
  40.     def url2pathname(pathname):
  41.         return pathname
  42.     def pathname2url(pathname):
  43.         return pathname
  44.  
  45. # This really consists of two pieces:
  46. # (1) a class which handles opening of all sorts of URLs
  47. #     (plus assorted utilities etc.)
  48. # (2) a set of functions for parsing URLs
  49. # XXX Should these be separated out into different modules?
  50.  
  51.  
  52. # Shortcut for basic usage
  53. _urlopener = None
  54. def urlopen(url, data=None):
  55.     global _urlopener
  56.     if not _urlopener:
  57.         _urlopener = FancyURLopener()
  58.     if data is None:
  59.         return _urlopener.open(url)
  60.     else:
  61.         return _urlopener.open(url, data)
  62. def urlretrieve(url, filename=None):
  63.     global _urlopener
  64.     if not _urlopener:
  65.         _urlopener = FancyURLopener()
  66.     if filename:
  67.         return _urlopener.retrieve(url, filename)
  68.     else:
  69.         return _urlopener.retrieve(url)
  70. def urlcleanup():
  71.     if _urlopener:
  72.         _urlopener.cleanup()
  73.  
  74.  
  75. # Class to open URLs.
  76. # This is a class rather than just a subroutine because we may need
  77. # more than one set of global protocol-specific options.
  78. # Note -- this is a base class for those who don't want the
  79. # automatic handling of errors type 302 (relocated) and 401
  80. # (authorization needed).
  81. ftpcache = {}
  82. class URLopener:
  83.  
  84.     __tempfiles = None
  85.  
  86.     # Constructor
  87.     def __init__(self, proxies=None):
  88.         if proxies is None:
  89.             proxies = getproxies()
  90.         assert hasattr(proxies, 'has_key'), "proxies must be a mapping"
  91.         self.proxies = proxies
  92.         server_version = "Python-urllib/%s" % __version__
  93.         self.addheaders = [('User-agent', server_version)]
  94.         self.__tempfiles = []
  95.         self.__unlink = os.unlink # See cleanup()
  96.         self.tempcache = None
  97.         # Undocumented feature: if you assign {} to tempcache,
  98.         # it is used to cache files retrieved with
  99.         # self.retrieve().  This is not enabled by default
  100.         # since it does not work for changing documents (and I
  101.         # haven't got the logic to check expiration headers
  102.         # yet).
  103.         self.ftpcache = ftpcache
  104.         # Undocumented feature: you can use a different
  105.         # ftp cache by assigning to the .ftpcache member;
  106.         # in case you want logically independent URL openers
  107.  
  108.     def __del__(self):
  109.         self.close()
  110.  
  111.     def close(self):
  112.         self.cleanup()
  113.  
  114.     def cleanup(self):
  115.         # This code sometimes runs when the rest of this module
  116.         # has already been deleted, so it can't use any globals
  117.         # or import anything.
  118.         if self.__tempfiles:
  119.             for file in self.__tempfiles:
  120.                 try:
  121.                     self.__unlink(file)
  122.                 except:
  123.                     pass
  124.             del self.__tempfiles[:]
  125.         if self.tempcache:
  126.             self.tempcache.clear()
  127.  
  128.     # Add a header to be used by the HTTP interface only
  129.     # e.g. u.addheader('Accept', 'sound/basic')
  130.     def addheader(self, *args):
  131.         self.addheaders.append(args)
  132.  
  133.     # External interface
  134.     # Use URLopener().open(file) instead of open(file, 'r')
  135.     def open(self, fullurl, data=None):
  136.         fullurl = unwrap(fullurl)
  137.         if self.tempcache and self.tempcache.has_key(fullurl):
  138.             filename, headers = self.tempcache[fullurl]
  139.             fp = open(filename, 'rb')
  140.             return addinfourl(fp, headers, fullurl)
  141.         type, url = splittype(fullurl)
  142.         if not type: type = 'file'
  143.         if self.proxies.has_key(type):
  144.             proxy = self.proxies[type]
  145.             type, proxy = splittype(proxy)
  146.             host, selector = splithost(proxy)
  147.             url = (host, fullurl) # Signal special case to open_*()
  148.         name = 'open_' + type
  149.         if '-' in name:
  150.             # replace - with _
  151.             name = string.join(string.split(name, '-'), '_')
  152.         if not hasattr(self, name):
  153.             if data is None:
  154.                 return self.open_unknown(fullurl)
  155.             else:
  156.                 return self.open_unknown(fullurl, data)
  157.         try:
  158.             if data is None:
  159.                 return getattr(self, name)(url)
  160.             else:
  161.                 return getattr(self, name)(url, data)
  162.         except socket.error, msg:
  163.             raise IOError, ('socket error', msg), sys.exc_info()[2]
  164.  
  165.     # Overridable interface to open unknown URL type
  166.     def open_unknown(self, fullurl, data=None):
  167.         type, url = splittype(fullurl)
  168.         raise IOError, ('url error', 'unknown url type', type)
  169.  
  170.     # External interface
  171.     # retrieve(url) returns (filename, None) for a local object
  172.     # or (tempfilename, headers) for a remote object
  173.     def retrieve(self, url, filename=None):
  174.         url = unwrap(url)
  175.         if self.tempcache and self.tempcache.has_key(url):
  176.             return self.tempcache[url]
  177.         type, url1 = splittype(url)
  178.         if not filename and (not type or type == 'file'):
  179.             try:
  180.                 fp = self.open_local_file(url1)
  181.                 hdrs = fp.info()
  182.                 del fp
  183.                 return url2pathname(splithost(url1)[1]), hdrs
  184.             except IOError, msg:
  185.                 pass
  186.         fp = self.open(url)
  187.         headers = fp.info()
  188.         if not filename:
  189.             import tempfile
  190.             garbage, path = splittype(url)
  191.             garbage, path = splithost(path or "")
  192.             path, garbage = splitquery(path or "")
  193.             path, garbage = splitattr(path or "")
  194.             suffix = os.path.splitext(path)[1]
  195.             filename = tempfile.mktemp(suffix)
  196.             self.__tempfiles.append(filename)
  197.         result = filename, headers
  198.         if self.tempcache is not None:
  199.             self.tempcache[url] = result
  200.         tfp = open(filename, 'wb')
  201.         bs = 1024*8
  202.         block = fp.read(bs)
  203.         while block:
  204.             tfp.write(block)
  205.             block = fp.read(bs)
  206.         fp.close()
  207.         tfp.close()
  208.         del fp
  209.         del tfp
  210.         return result
  211.  
  212.     # Each method named open_<type> knows how to open that type of URL
  213.  
  214.     # Use HTTP protocol
  215.     def open_http(self, url, data=None):
  216.         import httplib
  217.         if type(url) is type(""):
  218.             host, selector = splithost(url)
  219.             user_passwd, host = splituser(host)
  220.             realhost = host
  221.         else:
  222.             host, selector = url
  223.             urltype, rest = splittype(selector)
  224.             user_passwd = None
  225.             if string.lower(urltype) != 'http':
  226.                 realhost = None
  227.             else:
  228.                 realhost, rest = splithost(rest)
  229.                 user_passwd, realhost = splituser(realhost)
  230.                 if user_passwd:
  231.                     selector = "%s://%s%s" % (urltype,
  232.                                   realhost,
  233.                                   rest)
  234.             #print "proxy via http:", host, selector
  235.         if not host: raise IOError, ('http error', 'no host given')
  236.         if user_passwd:
  237.             import base64
  238.             auth = string.strip(base64.encodestring(user_passwd))
  239.         else:
  240.             auth = None
  241.         h = httplib.HTTP(host)
  242.         if data is not None:
  243.             h.putrequest('POST', selector)
  244.             h.putheader('Content-type',
  245.                     'application/x-www-form-urlencoded')
  246.             h.putheader('Content-length', '%d' % len(data))
  247.         else:
  248.             h.putrequest('GET', selector)
  249.         if auth: h.putheader('Authorization', 'Basic %s' % auth)
  250.         if realhost: h.putheader('Host', realhost)
  251.         for args in self.addheaders: apply(h.putheader, args)
  252.         h.endheaders()
  253.         if data is not None:
  254.             h.send(data + '\r\n')
  255.         errcode, errmsg, headers = h.getreply()
  256.         fp = h.getfile()
  257.         if errcode == 200:
  258.             return addinfourl(fp, headers, "http:" + url)
  259.         else:
  260.             return self.http_error(url,
  261.                            fp, errcode, errmsg, headers)
  262.  
  263.     # Handle http errors.
  264.     # Derived class can override this, or provide specific handlers
  265.     # named http_error_DDD where DDD is the 3-digit error code
  266.     def http_error(self, url, fp, errcode, errmsg, headers):
  267.         # First check if there's a specific handler for this error
  268.         name = 'http_error_%d' % errcode
  269.         if hasattr(self, name):
  270.             method = getattr(self, name)
  271.             result = method(url, fp, errcode, errmsg, headers)
  272.             if result: return result
  273.         return self.http_error_default(
  274.             url, fp, errcode, errmsg, headers)
  275.  
  276.     # Default http error handler: close the connection and raises IOError
  277.     def http_error_default(self, url, fp, errcode, errmsg, headers):
  278.         void = fp.read()
  279.         fp.close()
  280.         raise IOError, ('http error', errcode, errmsg, headers)
  281.  
  282.     # Use Gopher protocol
  283.     def open_gopher(self, url):
  284.         import gopherlib
  285.         host, selector = splithost(url)
  286.         if not host: raise IOError, ('gopher error', 'no host given')
  287.         type, selector = splitgophertype(selector)
  288.         selector, query = splitquery(selector)
  289.         selector = unquote(selector)
  290.         if query:
  291.             query = unquote(query)
  292.             fp = gopherlib.send_query(selector, query, host)
  293.         else:
  294.             fp = gopherlib.send_selector(selector, host)
  295.         return addinfourl(fp, noheaders(), "gopher:" + url)
  296.  
  297.     # Use local file or FTP depending on form of URL
  298.     def open_file(self, url):
  299.         if url[:2] == '//' and url[2:3] != '/':
  300.             return self.open_ftp(url)
  301.         else:
  302.             return self.open_local_file(url)
  303.  
  304.     # Use local file
  305.     def open_local_file(self, url):
  306.         import mimetypes, mimetools, StringIO
  307.         mtype = mimetypes.guess_type(url)[0]
  308.         headers = mimetools.Message(StringIO.StringIO(
  309.             'Content-Type: %s\n' % (mtype or 'text/plain')))
  310.         host, file = splithost(url)
  311.         if not host:
  312.             return addinfourl(
  313.                 open(url2pathname(file), 'rb'),
  314.                 headers, 'file:'+file)
  315.         host, port = splitport(host)
  316.         if not port and socket.gethostbyname(host) in (
  317.               localhost(), thishost()):
  318.             file = unquote(file)
  319.             return addinfourl(
  320.                 open(url2pathname(file), 'rb'),
  321.                 headers, 'file:'+file)
  322.         raise IOError, ('local file error', 'not on local host')
  323.  
  324.     # Use FTP protocol
  325.     def open_ftp(self, url):
  326.         host, path = splithost(url)
  327.         if not host: raise IOError, ('ftp error', 'no host given')
  328.         host, port = splitport(host)
  329.         user, host = splituser(host)
  330.         if user: user, passwd = splitpasswd(user)
  331.         else: passwd = None
  332.         host = socket.gethostbyname(host)
  333.         if not port:
  334.             import ftplib
  335.             port = ftplib.FTP_PORT
  336.         else:
  337.             port = int(port)
  338.         path, attrs = splitattr(path)
  339.         dirs = string.splitfields(path, '/')
  340.         dirs, file = dirs[:-1], dirs[-1]
  341.         if dirs and not dirs[0]: dirs = dirs[1:]
  342.         key = (user, host, port, string.joinfields(dirs, '/'))
  343.         if len(self.ftpcache) > MAXFTPCACHE:
  344.             # Prune the cache, rather arbitrarily
  345.             for k in self.ftpcache.keys():
  346.                 if k != key:
  347.                     v = self.ftpcache[k]
  348.                     del self.ftpcache[k]
  349.                     v.close()
  350.         try:
  351.             if not self.ftpcache.has_key(key):
  352.                 self.ftpcache[key] = \
  353.                     ftpwrapper(user, passwd,
  354.                            host, port, dirs)
  355.             if not file: type = 'D'
  356.             else: type = 'I'
  357.             for attr in attrs:
  358.                 attr, value = splitvalue(attr)
  359.                 if string.lower(attr) == 'type' and \
  360.                    value in ('a', 'A', 'i', 'I', 'd', 'D'):
  361.                     type = string.upper(value)
  362.             return addinfourl(
  363.                 self.ftpcache[key].retrfile(file, type),
  364.                 noheaders(), "ftp:" + url)
  365.         except ftperrors(), msg:
  366.             raise IOError, ('ftp error', msg), sys.exc_info()[2]
  367.  
  368.     # Use "data" URL
  369.     def open_data(self, url, data=None):
  370.         # ignore POSTed data
  371.         #
  372.         # syntax of data URLs:
  373.         # dataurl   := "data:" [ mediatype ] [ ";base64" ] "," data
  374.         # mediatype := [ type "/" subtype ] *( ";" parameter )
  375.         # data      := *urlchar
  376.         # parameter := attribute "=" value
  377.         import StringIO, mimetools, time
  378.         try:
  379.             [type, data] = string.split(url, ',', 1)
  380.         except ValueError:
  381.             raise IOError, ('data error', 'bad data URL')
  382.         if not type:
  383.             type = 'text/plain;charset=US-ASCII'
  384.         semi = string.rfind(type, ';')
  385.         if semi >= 0 and '=' not in type[semi:]:
  386.             encoding = type[semi+1:]
  387.             type = type[:semi]
  388.         else:
  389.             encoding = ''
  390.         msg = []
  391.         msg.append('Date: %s'%time.strftime('%a, %d %b %Y %T GMT',
  392.                             time.gmtime(time.time())))
  393.         msg.append('Content-type: %s' % type)
  394.         if encoding == 'base64':
  395.             import base64
  396.             data = base64.decodestring(data)
  397.         else:
  398.             data = unquote(data)
  399.         msg.append('Content-length: %d' % len(data))
  400.         msg.append('')
  401.         msg.append(data)
  402.         msg = string.join(msg, '\n')
  403.         f = StringIO.StringIO(msg)
  404.         headers = mimetools.Message(f, 0)
  405.         f.fileno = None        # needed for addinfourl
  406.         return addinfourl(f, headers, url)
  407.  
  408.  
  409. # Derived class with handlers for errors we can handle (perhaps)
  410. class FancyURLopener(URLopener):
  411.  
  412.     def __init__(self, *args):
  413.         apply(URLopener.__init__, (self,) + args)
  414.         self.auth_cache = {}
  415.  
  416.     # Default error handling -- don't raise an exception
  417.     def http_error_default(self, url, fp, errcode, errmsg, headers):
  418.         return addinfourl(fp, headers, "http:" + url)
  419.  
  420.     # Error 302 -- relocated (temporarily)
  421.     def http_error_302(self, url, fp, errcode, errmsg, headers):
  422.         # XXX The server can force infinite recursion here!
  423.         if headers.has_key('location'):
  424.             newurl = headers['location']
  425.         elif headers.has_key('uri'):
  426.             newurl = headers['uri']
  427.         else:
  428.             return
  429.         void = fp.read()
  430.         fp.close()
  431.         return self.open(newurl)
  432.  
  433.     # Error 301 -- also relocated (permanently)
  434.     http_error_301 = http_error_302
  435.  
  436.     # Error 401 -- authentication required
  437.     # See this URL for a description of the basic authentication scheme:
  438.     # http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt
  439.     def http_error_401(self, url, fp, errcode, errmsg, headers):
  440.         if headers.has_key('www-authenticate'):
  441.             stuff = headers['www-authenticate']
  442.             import re
  443.             match = re.match(
  444.                 '[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', stuff)
  445.             if match:
  446.                 scheme, realm = match.groups()
  447.                 if string.lower(scheme) == 'basic':
  448.                     return self.retry_http_basic_auth(
  449.                         url, realm)
  450.  
  451.     def retry_http_basic_auth(self, url, realm):
  452.         host, selector = splithost(url)
  453.         i = string.find(host, '@') + 1
  454.         host = host[i:]
  455.         user, passwd = self.get_user_passwd(host, realm, i)
  456.         if not (user or passwd): return None
  457.         host = user + ':' + passwd + '@' + host
  458.         newurl = '//' + host + selector
  459.         return self.open_http(newurl)
  460.  
  461.     def get_user_passwd(self, host, realm, clear_cache = 0):
  462.         key = realm + '@' + string.lower(host)
  463.         if self.auth_cache.has_key(key):
  464.             if clear_cache:
  465.                 del self.auth_cache[key]
  466.             else:
  467.                 return self.auth_cache[key]
  468.         user, passwd = self.prompt_user_passwd(host, realm)
  469.         if user or passwd: self.auth_cache[key] = (user, passwd)
  470.         return user, passwd
  471.  
  472.     def prompt_user_passwd(self, host, realm):
  473.         # Override this in a GUI environment!
  474.         try:
  475.             user = raw_input("Enter username for %s at %s: " %
  476.                      (realm, host))
  477.             self.echo_off()
  478.             try:
  479.                 passwd = raw_input(
  480.                   "Enter password for %s in %s at %s: " %
  481.                   (user, realm, host))
  482.             finally:
  483.                 self.echo_on()
  484.             return user, passwd
  485.         except KeyboardInterrupt:
  486.             return None, None
  487.  
  488.     def echo_off(self):
  489.         # XXX Is this sufficient???
  490.         if hasattr(os, "system"):
  491.             os.system("stty -echo")
  492.  
  493.     def echo_on(self):
  494.         # XXX Is this sufficient???
  495.         if hasattr(os, "system"):
  496.             print
  497.             os.system("stty echo")
  498.  
  499.  
  500. # Utility functions
  501.  
  502. # Return the IP address of the magic hostname 'localhost'
  503. _localhost = None
  504. def localhost():
  505.     global _localhost
  506.     if not _localhost:
  507.         _localhost = socket.gethostbyname('localhost')
  508.     return _localhost
  509.  
  510. # Return the IP address of the current host
  511. _thishost = None
  512. def thishost():
  513.     global _thishost
  514.     if not _thishost:
  515.         _thishost = socket.gethostbyname(socket.gethostname())
  516.     return _thishost
  517.  
  518. # Return the set of errors raised by the FTP class
  519. _ftperrors = None
  520. def ftperrors():
  521.     global _ftperrors
  522.     if not _ftperrors:
  523.         import ftplib
  524.         _ftperrors = ftplib.all_errors
  525.     return _ftperrors
  526.  
  527. # Return an empty mimetools.Message object
  528. _noheaders = None
  529. def noheaders():
  530.     global _noheaders
  531.     if not _noheaders:
  532.         import mimetools
  533.         import StringIO
  534.         _noheaders = mimetools.Message(StringIO.StringIO(), 0)
  535.         _noheaders.fp.close()    # Recycle file descriptor
  536.     return _noheaders
  537.  
  538.  
  539. # Utility classes
  540.  
  541. # Class used by open_ftp() for cache of open FTP connections
  542. class ftpwrapper:
  543.     def __init__(self, user, passwd, host, port, dirs):
  544.         self.user = unquote(user or '')
  545.         self.passwd = unquote(passwd or '')
  546.         self.host = host
  547.         self.port = port
  548.         self.dirs = []
  549.         for dir in dirs:
  550.             self.dirs.append(unquote(dir))
  551.         self.init()
  552.     def init(self):
  553.         import ftplib
  554.         self.busy = 0
  555.         self.ftp = ftplib.FTP()
  556.         self.ftp.connect(self.host, self.port)
  557.         self.ftp.login(self.user, self.passwd)
  558.         for dir in self.dirs:
  559.             self.ftp.cwd(dir)
  560.     def retrfile(self, file, type):
  561.         import ftplib
  562.         self.endtransfer()
  563.         if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1
  564.         else: cmd = 'TYPE ' + type; isdir = 0
  565.         try:
  566.             self.ftp.voidcmd(cmd)
  567.         except ftplib.all_errors:
  568.             self.init()
  569.             self.ftp.voidcmd(cmd)
  570.         conn = None
  571.         if file and not isdir:
  572.             # Use nlst to see if the file exists at all
  573.             try:
  574.                 self.ftp.nlst(file)
  575.             except ftplib.error_perm, reason:
  576.                 raise IOError, ('ftp error', reason), \
  577.                       sys.exc_info()[2]
  578.             # Restore the transfer mode!
  579.             self.ftp.voidcmd(cmd)
  580.             # Try to retrieve as a file
  581.             try:
  582.                 cmd = 'RETR ' + file
  583.                 conn = self.ftp.transfercmd(cmd)
  584.             except ftplib.error_perm, reason:
  585.                 if reason[:3] != '550':
  586.                     raise IOError, ('ftp error', reason), \
  587.                           sys.exc_info()[2]
  588.         if not conn:
  589.             # Set transfer mode to ASCII!
  590.             self.ftp.voidcmd('TYPE A')
  591.             # Try a directory listing
  592.             if file: cmd = 'LIST ' + file
  593.             else: cmd = 'LIST'
  594.             conn = self.ftp.transfercmd(cmd)
  595.         self.busy = 1
  596.         return addclosehook(conn.makefile('rb'), self.endtransfer)
  597.     def endtransfer(self):
  598.         if not self.busy:
  599.             return
  600.         self.busy = 0
  601.         try:
  602.             self.ftp.voidresp()
  603.         except ftperrors():
  604.             pass
  605.     def close(self):
  606.         self.endtransfer()
  607.         try:
  608.             self.ftp.close()
  609.         except ftperrors():
  610.             pass
  611.  
  612. # Base class for addinfo and addclosehook
  613. class addbase:
  614.     def __init__(self, fp):
  615.         self.fp = fp
  616.         self.read = self.fp.read
  617.         self.readline = self.fp.readline
  618.         self.readlines = self.fp.readlines
  619.         self.fileno = self.fp.fileno
  620.     def __repr__(self):
  621.         return '<%s at %s whose fp = %s>' % (
  622.               self.__class__.__name__, `id(self)`, `self.fp`)
  623.     def close(self):
  624.         self.read = None
  625.         self.readline = None
  626.         self.readlines = None
  627.         self.fileno = None
  628.         if self.fp: self.fp.close()
  629.         self.fp = None
  630.  
  631. # Class to add a close hook to an open file
  632. class addclosehook(addbase):
  633.     def __init__(self, fp, closehook, *hookargs):
  634.         addbase.__init__(self, fp)
  635.         self.closehook = closehook
  636.         self.hookargs = hookargs
  637.     def close(self):
  638.         if self.closehook:
  639.             apply(self.closehook, self.hookargs)
  640.             self.closehook = None
  641.             self.hookargs = None
  642.         addbase.close(self)
  643.  
  644. # class to add an info() method to an open file
  645. class addinfo(addbase):
  646.     def __init__(self, fp, headers):
  647.         addbase.__init__(self, fp)
  648.         self.headers = headers
  649.     def info(self):
  650.         return self.headers
  651.  
  652. # class to add info() and geturl() methods to an open file
  653. class addinfourl(addbase):
  654.     def __init__(self, fp, headers, url):
  655.         addbase.__init__(self, fp)
  656.         self.headers = headers
  657.         self.url = url
  658.     def info(self):
  659.         return self.headers
  660.     def geturl(self):
  661.         return self.url
  662.  
  663.  
  664. # Utility to combine a URL with a base URL to form a new URL
  665.  
  666. def basejoin(base, url):
  667.     type, path = splittype(url)
  668.     if type:
  669.         # if url is complete (i.e., it contains a type), return it
  670.         return url
  671.     host, path = splithost(path)
  672.     type, basepath = splittype(base) # inherit type from base
  673.     if host:
  674.         # if url contains host, just inherit type
  675.         if type: return type + '://' + host + path
  676.         else:
  677.             # no type inherited, so url must have started with //
  678.             # just return it
  679.             return url
  680.     host, basepath = splithost(basepath) # inherit host
  681.     basepath, basetag = splittag(basepath) # remove extraneuous cruft
  682.     basepath, basequery = splitquery(basepath) # idem
  683.     if path[:1] != '/':
  684.         # non-absolute path name
  685.         if path[:1] in ('#', '?'):
  686.             # path is just a tag or query, attach to basepath
  687.             i = len(basepath)
  688.         else:
  689.             # else replace last component
  690.             i = string.rfind(basepath, '/')
  691.         if i < 0:
  692.             # basepath not absolute
  693.             if host:
  694.                 # host present, make absolute
  695.                 basepath = '/'
  696.             else:
  697.                 # else keep non-absolute
  698.                 basepath = ''
  699.         else:
  700.             # remove last file component
  701.             basepath = basepath[:i+1]
  702.         # Interpret ../ (important because of symlinks)
  703.         while basepath and path[:3] == '../':
  704.             path = path[3:]
  705.             i = string.rfind(basepath[:-1], '/')
  706.             if i > 0:
  707.                 basepath = basepath[:i+1]
  708.             elif i == 0:
  709.                 basepath = '/'
  710.                 break
  711.             else:
  712.                 basepath = ''
  713.             
  714.         path = basepath + path
  715.     if type and host: return type + '://' + host + path
  716.     elif type: return type + ':' + path
  717.     elif host: return '//' + host + path # don't know what this means
  718.     else: return path
  719.  
  720.  
  721. # Utilities to parse URLs (most of these return None for missing parts):
  722. # unwrap('<URL:type://host/path>') --> 'type://host/path'
  723. # splittype('type:opaquestring') --> 'type', 'opaquestring'
  724. # splithost('//host[:port]/path') --> 'host[:port]', '/path'
  725. # splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'
  726. # splitpasswd('user:passwd') -> 'user', 'passwd'
  727. # splitport('host:port') --> 'host', 'port'
  728. # splitquery('/path?query') --> '/path', 'query'
  729. # splittag('/path#tag') --> '/path', 'tag'
  730. # splitattr('/path;attr1=value1;attr2=value2;...') ->
  731. #   '/path', ['attr1=value1', 'attr2=value2', ...]
  732. # splitvalue('attr=value') --> 'attr', 'value'
  733. # splitgophertype('/Xselector') --> 'X', 'selector'
  734. # unquote('abc%20def') -> 'abc def'
  735. # quote('abc def') -> 'abc%20def')
  736.  
  737. def unwrap(url):
  738.     url = string.strip(url)
  739.     if url[:1] == '<' and url[-1:] == '>':
  740.         url = string.strip(url[1:-1])
  741.     if url[:4] == 'URL:': url = string.strip(url[4:])
  742.     return url
  743.  
  744. _typeprog = None
  745. def splittype(url):
  746.     global _typeprog
  747.     if _typeprog is None:
  748.         import re
  749.         _typeprog = re.compile('^([^/:]+):')
  750.  
  751.     match = _typeprog.match(url)
  752.     if match:
  753.         scheme = match.group(1)
  754.         return scheme, url[len(scheme) + 1:]
  755.     return None, url
  756.  
  757. _hostprog = None
  758. def splithost(url):
  759.     global _hostprog
  760.     if _hostprog is None:
  761.         import re
  762.         _hostprog = re.compile('^//([^/]+)(.*)$')
  763.  
  764.     match = _hostprog.match(url) 
  765.     if match: return match.group(1, 2)
  766.     return None, url
  767.  
  768. _userprog = None
  769. def splituser(host):
  770.     global _userprog
  771.     if _userprog is None:
  772.         import re
  773.         _userprog = re.compile('^([^@]*)@(.*)$')
  774.  
  775.     match = _userprog.match(host)
  776.     if match: return match.group(1, 2)
  777.     return None, host
  778.  
  779. _passwdprog = None
  780. def splitpasswd(user):
  781.     global _passwdprog
  782.     if _passwdprog is None:
  783.         import re
  784.         _passwdprog = re.compile('^([^:]*):(.*)$')
  785.  
  786.     match = _passwdprog.match(user)
  787.     if match: return match.group(1, 2)
  788.     return user, None
  789.  
  790. _portprog = None
  791. def splitport(host):
  792.     global _portprog
  793.     if _portprog is None:
  794.         import re
  795.         _portprog = re.compile('^(.*):([0-9]+)$')
  796.  
  797.     match = _portprog.match(host)
  798.     if match: return match.group(1, 2)
  799.     return host, None
  800.  
  801. # Split host and port, returning numeric port.
  802. # Return given default port if no ':' found; defaults to -1.
  803. # Return numerical port if a valid number are found after ':'.
  804. # Return None if ':' but not a valid number.
  805. _nportprog = None
  806. def splitnport(host, defport=-1):
  807.     global _nportprog
  808.     if _nportprog is None:
  809.         import re
  810.         _nportprog = re.compile('^(.*):(.*)$')
  811.  
  812.     match = _nportprog.match(host)
  813.     if match:
  814.         host, port = match.group(1, 2)
  815.         try:
  816.             if not port: raise string.atoi_error, "no digits"
  817.             nport = string.atoi(port)
  818.         except string.atoi_error:
  819.             nport = None
  820.         return host, nport
  821.     return host, defport
  822.  
  823. _queryprog = None
  824. def splitquery(url):
  825.     global _queryprog
  826.     if _queryprog is None:
  827.         import re
  828.         _queryprog = re.compile('^(.*)\?([^?]*)$')
  829.  
  830.     match = _queryprog.match(url)
  831.     if match: return match.group(1, 2)
  832.     return url, None
  833.  
  834. _tagprog = None
  835. def splittag(url):
  836.     global _tagprog
  837.     if _tagprog is None:
  838.         import re
  839.         _tagprog = re.compile('^(.*)#([^#]*)$')
  840.  
  841.     match = _tagprog.match(url)
  842.     if match: return match.group(1, 2)
  843.     return url, None
  844.  
  845. def splitattr(url):
  846.     words = string.splitfields(url, ';')
  847.     return words[0], words[1:]
  848.  
  849. _valueprog = None
  850. def splitvalue(attr):
  851.     global _valueprog
  852.     if _valueprog is None:
  853.         import re
  854.         _valueprog = re.compile('^([^=]*)=(.*)$')
  855.  
  856.     match = _valueprog.match(attr)
  857.     if match: return match.group(1, 2)
  858.     return attr, None
  859.  
  860. def splitgophertype(selector):
  861.     if selector[:1] == '/' and selector[1:2]:
  862.         return selector[1], selector[2:]
  863.     return None, selector
  864.  
  865. _quoteprog = None
  866. def unquote(s):
  867.     global _quoteprog
  868.     if _quoteprog is None:
  869.         import re
  870.         _quoteprog = re.compile('%[0-9a-fA-F][0-9a-fA-F]')
  871.  
  872.     i = 0
  873.     n = len(s)
  874.     res = []
  875.     while 0 <= i < n:
  876.         match = _quoteprog.search(s, i)
  877.         if not match:
  878.             res.append(s[i:])
  879.             break
  880.         j = match.start(0)
  881.         res.append(s[i:j] + chr(string.atoi(s[j+1:j+3], 16)))
  882.         i = j+3
  883.     return string.joinfields(res, '')
  884.  
  885. def unquote_plus(s):
  886.     if '+' in s:
  887.         # replace '+' with ' '
  888.         s = string.join(string.split(s, '+'), ' ')
  889.     return unquote(s)
  890.  
  891. always_safe = string.letters + string.digits + '_,.-'
  892. def quote(s, safe = '/'):
  893.     safe = always_safe + safe
  894.     res = []
  895.     for c in s:
  896.         if c in safe:
  897.             res.append(c)
  898.         else:
  899.             res.append('%%%02x' % ord(c))
  900.     return string.joinfields(res, '')
  901.  
  902. def quote_plus(s, safe = '/'):
  903.     if ' ' in s:
  904.         # replace ' ' with '+'
  905.         s = string.join(string.split(s, ' '), '+')
  906.         return quote(s, safe + '+')
  907.     else:
  908.         return quote(s, safe)
  909.  
  910.  
  911. # Proxy handling
  912. def getproxies():
  913.     """Return a dictionary of protocol scheme -> proxy server URL mappings.
  914.  
  915.     Scan the environment for variables named <scheme>_proxy;
  916.     this seems to be the standard convention.  If you need a
  917.     different way, you can pass a proxies dictionary to the
  918.     [Fancy]URLopener constructor.
  919.  
  920.     """
  921.     proxies = {}
  922.     for name, value in os.environ.items():
  923.         name = string.lower(name)
  924.         if value and name[-6:] == '_proxy':
  925.             proxies[name[:-6]] = value
  926.     return proxies
  927.  
  928.  
  929. # Test and time quote() and unquote()
  930. def test1():
  931.     import time
  932.     s = ''
  933.     for i in range(256): s = s + chr(i)
  934.     s = s*4
  935.     t0 = time.time()
  936.     qs = quote(s)
  937.     uqs = unquote(qs)
  938.     t1 = time.time()
  939.     if uqs != s:
  940.         print 'Wrong!'
  941.     print `s`
  942.     print `qs`
  943.     print `uqs`
  944.     print round(t1 - t0, 3), 'sec'
  945.  
  946.  
  947. # Test program
  948. def test():
  949.     import sys
  950.     args = sys.argv[1:]
  951.     if not args:
  952.         args = [
  953.             '/etc/passwd',
  954.             'file:/etc/passwd',
  955.             'file://localhost/etc/passwd',
  956.             'ftp://ftp.python.org/etc/passwd',
  957.             'gopher://gopher.micro.umn.edu/1/',
  958.             'http://www.python.org/index.html',
  959.             ]
  960.     try:
  961.         for url in args:
  962.             print '-'*10, url, '-'*10
  963.             fn, h = urlretrieve(url)
  964.             print fn, h
  965.             if h:
  966.                 print '======'
  967.                 for k in h.keys(): print k + ':', h[k]
  968.                 print '======'
  969.             fp = open(fn, 'rb')
  970.             data = fp.read()
  971.             del fp
  972.             if '\r' in data:
  973.                 table = string.maketrans("", "")
  974.                 data = string.translate(data, table, "\r")
  975.             print data
  976.             fn, h = None, None
  977.         print '-'*40
  978.     finally:
  979.         urlcleanup()
  980.  
  981. # Run test program when run as a script
  982. if __name__ == '__main__':
  983.     test1()
  984.     test()
  985.